In [1]:
import numpy as np # because arrays are defined in numpy
from numba import njit, prange # This is the new line with numba
@njit # this is an alias for @jit(nopython=True)
def Mand(z0, max_steps):
z = 0j # no need to specify type.
# To initialize to complex number, just assign 0j==i*0
for itr in range(max_steps):
if abs(z)>2:
return itr
z = z*z + z0
return max_steps
@njit(parallel=True)
def Mandelbrot3(data, ext, max_steps):
"""
ext[4] -- array of 4 values [min_x,max_x,min_y,max_y]
Nxy -- int number of points in x and y direction
max_steps -- how many steps we will try at most before we conclude the point is in the set
"""
Nx,Ny = data.shape # 2D array should be already allocated we get its size
for i in prange(Nx):
for j in range(Ny): # note that we used prange instead of range.
# this switches off parallelization of this loop, so that
# only the outside loop over i is parallelized.
x = ext[0] + (ext[1]-ext[0])*i/(Nx-1.)
y = ext[2] + (ext[3]-ext[2])*j/(Ny-1.)
# creating complex number of the fly
data[i,j] = Mand(x + y*1j, max_steps)
# data now contains integers.
# MandelbrotSet has value 1000, and points not in the set have value <1000.
In [3]:
%matplotlib widget
# Tells Jupyter to use the ipympl widget backend instead of the default inline backend.
# The default backend (inline) renders static PNG images.
import matplotlib.pyplot as plt
import matplotlib
import matplotlib.cm as cm
data = np.zeros((800, 800), dtype=np.int32)
ext = np.array([-2.0, 1.0, -1.0, 1.0], dtype=np.float64)
# compile once
Mandelbrot3(data, ext, 1000)
# Interactive callbacks are always attached to a Figure canvas.
fig, ax = plt.subplots()
im = ax.imshow(-np.log(data.T + 1), extent=ext, origin="lower",
cmap=cm.coolwarm, aspect="equal", interpolation="nearest")
# extent=ext -> Maps pixel coordinates → physical coordinates in the complex plane
# origin="lower" -> Ensures correct orientation (positive imaginary axis upward)
# aspect="equal" -> Prevents geometric distortion during zoom
# interpolation="nearest" -> Prevents smoothing (important for fractals)
# imshow returns an image object (im)
# This object can later be updated in place using: im.set_data(...), im.set_extent(...)
# ax stores the current view limits (xlim, ylim) that define the zoomed region.
ax.set_title("Pan/zoom, release mouse to recompute")
# Prevents multiple Mandelbrot recomputations from running at the same time.
# Zooming triggers multiple events.
# Without protection callbacks can recursively call themselves
# the notebook can freeze
# This flag ensures only one recomputation runs at a time.
# takeaway: Event-driven code must be re-entrancy safe.
_busy = False
def recompute_from_axes():
global _busy, ext # using global variables to see if something is already plotting
if _busy:
return
_busy = True
# try:
# # code that might fail or return early
# finally:
# # code that MUST run no matter what
try:
x0, x1 = ax.get_xlim() # Reading the zoomed region from the axes
y0, y1 = ax.get_ylim()
# new region of the complex plane.
ext = np.array([x0, x1, y0, y1], dtype=np.float64)
# new calculation on the new region
Mandelbrot3(data, ext, 1000)
# Updating the existing image (not recreating it!)
im.set_data(-np.log(data.T + 1)) # Replaces the image’s pixel data
im.set_extent(ext) # Updates the coordinate mapping
fig.canvas.draw_idle() # Requests a redraw when the event loop is idle.
# Creating a new imshow object every time would be slow and leak memory
finally:
_busy = False
def on_release(event):
# only respond to releases inside our axes
if event.inaxes is ax:
# Ensures the callback only fires when the mouse action occurs inside the plot.
# Prevents spurious recomputation when clicking UI elements or margins.
recompute_from_axes()
# Mouse event hookup:
# Registers a callback function to be executed when the mouse button is released.
cid = fig.canvas.mpl_connect("button_release_event", on_release)
plt.show()
In [ ]: